哈囉,各位邦友們!
昨天我們借助 resource() API 整頓了 Heroes 專案的非同步狀態,感受 Signals 與 RxJS 攜手合作的威力。
今天換個角度,回到「專案骨架」本身,聊聊 Angular 從 NgModule 演進到 Standalone 的歷史,也複習我們專案一開始便採用的 Standalone 架構與實作細節。
NgModule 時代的設計思維與常見痛點。resource() 重構,對 bootstrapApplication 組態與 AppConfig 提供者不陌生。provideRouter 實作。NgModule 時代:集中註冊帶來的限制過去 Angular 依賴 NgModule 進行組態:所有元件、指令、管線必須先被宣告 (declarations),再由 imports 共享給其他模組。典型啟動流程如下:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { HeroesModule } from './heroes/heroes.module';
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HeroesModule],
  bootstrap: [AppComponent],
})
export class AppModule {}
這套模式在大型應用很受歡迎,但也帶來幾個實務痛點:
declarations 或 exports 而卡關。為了降低學習門檻,Angular 在 v14~v15 正式釋出 Standalone 元件,讓每個元件可以自帶 imports,應用入口也不再需要 NgModule。
Hero Journey 專案自一開始就採用 CLI 的 Standalone 模板,main.ts 透過 bootstrapApplication() 啟動應用:
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
AppConfig 則集中提供路由、HTTP、SSR 與 zoneless 設定:
import { ApplicationConfig, importProvidersFrom, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { routes } from './app.routes';
import { InMemoryData } from './in-memory-data';
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
    provideBrowserGlobalErrorListeners(),
    provideZonelessChangeDetection(),
    provideHttpClient(withFetch()),
    importProvidersFrom(HttpClientInMemoryWebApiModule.forRoot(InMemoryData, {
      dataEncapsulation: false,
      delay: 300,
      post204: false,
      put204: false,
    })),
    provideClientHydration(withEventReplay()),
  ],
};
在 Standalone 世界裡:
imports 使用其他 Standalone 元件或路由、表單等模組,一次完成紀錄與分享。provideRouter、provideHttpClient 等函式化 API,讓 Day10、Day12 的設定改為純函式呼叫,更好拆測試與重構。因為我們從一開始就已經從 Standalone 架構出發,今天就當成一次複習,確認專案的每個重要環節。
main.ts 透過 bootstrapApplication() 直接引導根元件,省去 NgModule。根元件 App 宣告 standalone: true 並明確列出依賴:@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterLink, RouterLinkActive, RouterOutlet],
  templateUrl: './app.html',
  styleUrl: './app.scss',
})
export class App {
  protected readonly title = signal('hero-journey');
}
app.config.ts 以 ApplicationConfig 包裝路由、HTTP、SSR 與 zoneless 設定,透過 provideRouter()、provideHttpClient()、importProvidersFrom() 等函式化 API 統一調整。我們在 Day12~Day23 完成的服務、表單與資源重構,皆使用這套提供者模型。standalone: true 與專屬 imports,需要時直接在使用端引入,降低測試與 Storybook 建置的心智負擔。ApplicationConfig,TestBed 或 Storybook 的元件測試只需要匯入同一份設定即可重建環境,與傳統 NgModule 相比更容易維護。保持這些原則,就能確保專案持續受惠於 Standalone 架構的模組化與清晰依賴關係。
main.ts 改以 bootstrapApplication() 啟動,AppModule 不再參與流程。imports 引入依賴,不再依附於 declarations。ApplicationConfig,測試環境能以相同函式組態重建。Angular 從 NgModule 走到 Standalone,最大的價值在把焦點從模組搬回元件與功能本身。
下一篇我們會延伸到 @defer,繼續探索現代 Angular 的效能優化能力。
bootstrapApplication: